#!node
//@ts-check

const Axios = require("axios").default
const fs = require('fs')
const JsonFormat = require('json-format')
const PgClient = require('pg').Client
const prompts = require('prompts')
const ConfirmPrompt = require("prompts/lib/elements/confirm")

/** @param {string} str */
function lengthLimit(str, length = 100) {
    if (length < 3) {
        length = 3
    }
    return (str.length > length) ? (str.substring(0, length - 3) + '...') : str
}

class FicsitQueryEngine {
    /** Axios agent */
    #agent
    /** @constant GraphQL hash */
    #H = {
        "GetAnnouncements": "ae9a985a6dc49555bb1a48691c4cd4e94b957328cb6647f588e2bf10cde064ca",
        "GetMod": "b1bd5b603f89fba35d39acdd122959bd91a78173798c342f11eb314ece60e8e5",
        "GetModReference": "b0a9271303b21eef53ed6f6018fdf774d8e2d78c4721dac1f2a9b6477ac38541",
        "GetMods": "e401ce89f96752d81ab2229c17422c2b9808d3afd75c6a4acc18432a46cb1313",
        "GetModVersion": "9115900e5d25c94b22aebe223f6d5afc8b7cb3574bcdf442f106e1e335812a68",
        "GetModVersions": "71fb981dd2e069cca09a116bb66d60e6f04c663d7d49ce87a5acd710f9c87c52",
        "GetSMLVersions": "9b0b582ccd5a80a2610a5e40c08cced4ccb47f590c7cd24ad5962fe8f7527774",
        "GetTags": "7c0b7fc5f6c51a493719f88a05a7f35adc974b2883aa348a763edf88abb88106",
        "GetUser": "7e32ee202d8b71db22e17fb7c3d07e493cac87e37863641d019e379bff926cf2",
    }

    /**
     * @param {string} baseURL Base URL of SMR API
     */
    constructor(baseURL) {
        this.#agent = Axios.create({ baseURL })
    }

    /** @param {string} baseURL */
    setBaseURL(baseURL) {
        this.#agent.defaults.baseURL = baseURL
    }

    /**
     * @param {string} oper Operation name
     * @param {object} variables Query variables
     */
    async query(oper, variables = undefined) {
        if (!this.#H.hasOwnProperty(oper)) {
            return null
        }
        let params = {
            operationName: oper,
            extensions: JSON.stringify({ persistedQuery: { sha256Hash: this.#H[oper], version: 1 } }),
        }
        if (variables) {
            params.variables = JSON.stringify(variables)
        }
        const res = await this.#agent.get("/query", { params })
        return res.data
    }
}

class FicsitQuery {
    #engine

    /**
    * @param {string} baseURL Base URL of SMR API
    */
    constructor(baseURL) {
        this.#engine = new FicsitQueryEngine(baseURL)
    }

    setBaseUrl(baseURL) {
        this.#engine.setBaseURL(baseURL)
    }

    async GetAnnouncements() {
        const res = await this.#engine.query("GetAnnouncements")
        return res.data?.getAnnouncements
    }

    /** @param {string} idOrRef Mod ID or Refenrence*/
    async GetMod(idOrRef) {
        if (!idOrRef) {
            return null
        }
        const res = await this.#engine.query("GetMod", { mod: idOrRef })
        return res.data?.mod
    }

    /**
     * @param {string[]} list A list of mod refenrence
     */
    async GetModBulk(list) {
        if (!Array.isArray(list) || list.length === 0) {
            return []
        }
        const res = await Promise.all(list.map(e => this.GetMod(e)))
        return res
    }

    /** @param {string?} search Search word */
    async GetMods(search) {
        if (!search) {
            search = null
        }
        const res = await this.#engine.query("GetMods", {
            "limit": 40, "offset": 0, "order": "desc", "orderBy": "last_version_date", search
        })
        return res.data?.getMods
    }

    /**
     * @param {string} mod Mod ref or id
     * @returns {Promise<null | {id: string, mod_reference: string, name: string}>}
     */
    async GetModReference(mod) {
        if (!mod) {
            return null
        }
        const res = await this.#engine.query("GetModReference", { mod })
        return res.data?.mod
    }

    /**
     * @param {string} mod Mod ID
     * @param {number} limit Versions limit
     */
    async GetModVersions(mod, limit = 100) {
        if (!mod) {
            return null
        }
        const res = await this.#engine.query("GetModVersions", { mod, limit, offset: 0 })
        return res.data?.getMod?.versions
    }
}

class RepoDatabase {
    /** Postgre Client */
    #pg

    constructor(options) {
        this.#pg = new PgClient(options)
        !(async () => {
            try {
                await this.#pg.connect()
                // console.log("database connected")
            } catch (err) {
                console.error(err)
                process.exit(1)
            }
        })()
    }

    async disconnect() {
        await this.#pg.end()
    }

    /**
     * @param {string} table
     * @param {string} key
     */
    async keyExists(table, key) {
        const res = await this.#pg.query(`select exists(select 1 from ${table} where id='${key}')`)
        return res?.rowCount && (res.rowCount > 0)
    }

    /**
     * @param {object} announcement
     * @param {string} announcement.id 
     * @param {string} announcement.message 
     * @param {string} announcement.importance "Alert" | "Fix"
     */
    async InsertAnnouncement(announcement) {
        if (await this.keyExists('announcements', announcement.id)) {
            console.log('announcement exists #', announcement.id)
            return
        }
        const query = {
            text: 'INSERT INTO announcements(id, message, importance, created_at, updated_at) VALUES($1, $2, $3, NOW(), NOW())',
            values: [announcement.id, announcement.message, announcement.importance],
        }
        const res = await this.#pg.query(query)
        console.log(res)
    }

    async getAllModRefs() {
        const query = {
            text: 'SELECT id, mod_reference AS ref FROM mods ORDER BY mod_reference'
        }
        const res = await this.#pg.query(query)
        return res.rows
    }

    /** @param {string} id */
    async getLatestVertion(id) {
        const query = {
            text: 'SELECT id, version FROM versions WHERE mod_id = $1 ORDER BY version_major DESC, version_minor DESC, version_patch DESC',
            values: [id]
        }
        const res = await this.#pg.query(query)
        // console.log(res.rows)
        return res.rows[0].version
    }
}

/**
 * @param {FicsitQuery} fq 
 * @param {RepoDatabase} db 
 * @param {{id: string, ref: string}} dbRef Mod reference
 * @param {boolean} autoSkip
 */
async function actionCheckUpdateMod(fq, db, dbRef, autoSkip = true) {
    /** @param {string} semver */
    function pv(semver) {
        const r = /^(\d+)\.(\d+)\.(\d+)(-\w+)?$/.exec(semver)
        return r === null ? -1 : (1e8 * Number(r[1]) + 1e4 * Number(r[2]) + Number(r[3]))
    }

    /** @returns {string} */
    function gv(versions) {
        const { alpha, beta, release } = versions
        const a = alpha ? pv(alpha.version) : -1
        const b = beta ? pv(beta.version) : -1
        const r = release ? pv(release.version) : -1
        if (r > a && r > b) {
            return release.version
        }
        else if (b > a) {
            return beta.version
        }
        else {
            return alpha.version
        }
    }
    const fmt = { type: 'space', size: 2 }

    const { id, ref } = dbRef
    const info = await fq.GetMod(ref)
    const verF = gv(info.latestVersions)
    const verD = await db.getLatestVertion(id)
    // console.log(ref, verF, verD)

    let verInfo = {}
    if (autoSkip && verF === verD) {
        new ConfirmPrompt({
            type: 'confirm',
            name: 'update',
            message: `[Mod] ${ref} ${verD} = ${verF}`,
            initial: false,
        })._("n")
    }
    else {
        const confirm = await prompts({
            type: 'confirm',
            name: 'update',
            message: `[Mod] ${ref} ${verD} → ${verF}`,
            initial: true
        })
        if (confirm.update) {
            verInfo = await fq.GetModVersions(info.id, 3)
        }
    }

    const file = `logs/${ref}.jsonc`
    fs.writeFileSync(file, `\
${JsonFormat({ url: 'https://ficsit.app/mod/' + ref, time: new Date().toLocaleString() }, fmt)}
${verInfo ? JsonFormat(verInfo, fmt) : '"up to date"'}
${JsonFormat(info, fmt)}
/*
${info.full_description}
*/`)
    return file
}

/**
 * Prompts
 * @param {FicsitQuery} fq 
 * @param {RepoDatabase} db 
 * @returns 0 to exit
 */
async function cli(fq, db) {
    const op = await prompts({
        type: 'autocomplete',
        name: 'operation',
        message: 'Action',
        choices: [
            { title: 'announcement' },
            { title: 'check update all' },
            { title: 'check update mod' },
            { title: 'quit' },
        ]
    })
    switch (op.operation) {
        case 'announcement': {
            const an = await fq.GetAnnouncements()
            const select = await prompts({
                type: 'select',
                name: 'index',
                message: 'Select an announcement to migrate',
                choices: an.map((v, k) => ({
                    title: `${v.importance}#${v.id}: ${lengthLimit(v.message, 50)}`,
                    value: k,
                })).concat([{ title: 'Cancel', value: -1 }]),
                initial: 0,
            })
            // console.log(select.index)
            if (select.index >= 0) {
                // console.log("GetAnnouncements", an)
                await db.InsertAnnouncement(an[select.index])
            }
            break
        }
        case 'check update all': {
            const refs = await db.getAllModRefs()
            // const info = await fq.GetModBulk(refs.map(e => e.ref))
            for (let i = 0; i < refs.length; i++) {
                await actionCheckUpdateMod(fq, db, refs[i])
            }
            break
        }
        case 'check update mod': {
            const refs = await db.getAllModRefs()
            const input = await prompts({
                type: 'autocomplete',
                name: 'modref',
                message: 'Input mod reference',
                choices: refs.map((v, k) => ({ title: v.ref, value: k }))
                    .concat([{ title: '[Cancel]', value: -1 }])
            })
            if (input.modref >= 0) {
                await actionCheckUpdateMod(fq, db, refs[input.modref])
            }
            break
        }
        case 'quit':
            return 0
        default:
            console.log("Unknown operation", op.operation)
            return 0
    }
    return 1
}

function getEnvVars(file) {
    let env = {}
    const envFile = fs.readFileSync(file, { encoding: 'utf8' })
    for (let line of envFile.split(/(\r?\n)+/g)) {
        line = line.trim()
        if (!line || line.startsWith("#")) continue
        const [k, v] = line.split('=')
        env[k] = v
    }
    // console.log("env", env)
    return env
}

async function main(argv) {
    const env = getEnvVars('ficsit.env')
    fs.mkdirSync("logs", { recursive: true })

    const db = new RepoDatabase(env.POSTGRES)
    const fq = new FicsitQuery("https://api.ficsit.app/v2")

    while (await cli(fq, db)) { }

    console.log("exit prompt")
    await db.disconnect()
}

main(process.argv)